package br.com.dyad.commons.data;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.collections.comparators.ComparableComparator;
import org.apache.commons.lang.StringUtils;
import org.josql.QueryResults;

import sun.reflect.Reflection;

import br.com.dyad.commons.data.events.DataListEvent;
import br.com.dyad.commons.data.events.DataListListener;
import br.com.dyad.commons.data.events.EntityPropertyEvent;
import br.com.dyad.commons.data.events.EntityPropertyListener;
import br.com.dyad.commons.exceptions.CommitException;
import br.com.dyad.commons.exceptions.DeleteException;
import br.com.dyad.commons.exceptions.SaveException;
import br.com.dyad.commons.reflect.ReflectUtil;
import br.com.dyad.commons.data.validation.UniqueValidation;

public class DataList implements EntityPropertyListener {
	private final static String FIRST = "A";
	private final static String LAST = "B";
	
	@SuppressWarnings("unchecked")
	/**
	 * Lista com os objetos deste dataList. 
	 * Importante: ela não deve ser manipulada fora do DataList com os metodos de add e remove.
	 * @see add
	 * @see delete
	 */
	private List<AppEntity> list;
	
	/**
	 * Lista interna que guarda todos os objetos que foram dado save até ser executado um commit.
	 */
	private HashMap<Long,AppEntity> savedEntities; 
	
	/**
	 * Lista interna que guarda todos os objetos que foram deletados até ser executado um commit.
	 */
	private HashMap<Long,AppEntity> deletedEntities; 

	/**
	 * Objeto interno que guarda as informacoes sobre indices.
	 */
	private HashMap<String, HashMapIndexList> hashMapIndexes;	
	
	/**
	 * Propriedade que garda a conexão com o banco de dados.
	 */
	private AppTransaction transactionalSession;

	/** 
	 * Propriedade que indica se deve ser dado um commit após um save de uma entidade. Default false.
	 */
	private Boolean commitOnSave;

	/**
	 * Propriedade que indica se os metodos de add, delete e post vao ser logadas no commit. Default true. 
	 */
	private Boolean logChanges;	
	
	/**
	 * Lista de observadores(Listener) do datalist 
	 */
	private ArrayList<DataListListener> dataListListeners = new ArrayList<DataListListener>(); 
	
	protected HashMap<String, DataListView> views;
	
	
	/**
	 * Default constructor. 
	 * O objeto é criado com a propriedade commitOnSave como false. Também é criado uma lista interna para armazenar as entidades que devem ser filhas de AppEntity;
	 * Não deve ser criado objetos diretamente desta classe. Deve-se usar a factory DataListFactory.
	 * @see  br.com.dyad.commons.data.DataListFactory
	 */
	public DataList() {
		this.setCommitOnSave(false);
		this.setLogChanges(true);
		this.setList( new ArrayList<AppEntity>() );
		this.savedEntities = new HashMap<Long,AppEntity>();
		this.deletedEntities = new HashMap<Long,AppEntity>(); 
		this.views = new HashMap<String,DataListView>();
	}
	
	/**
	 * Cria um DataList recebendo a AppTransaction como parametro. Isto é útil quando queremos gravar varios dataList numa mesma transação.
	 * Não deve ser criado objetos diretamente desta classe. Deve-se usar a factory DataListFactory.
	 * @see br.com.dyad.commons.data.DataListFactory
	 */
	public DataList(AppTransaction transactionalSession) {
		this();
		this.setTransactionalSession( transactionalSession );
	}

	@SuppressWarnings("unchecked")
	/**
	 * Retorna a lista interna da DataList. IMPORTANTE: a lista não deve ser manipulada diretamente através de add e removes.
	 * Deve-se chamar os metodos relativos para adição e remoção: add e remove. Isto é necessário pois estes métodos mantém íntegros os índices de busca.
	 * Se alguém remover manualmente um elemento da lista, os índices ficarão quebradas apontando para um elemento que não existe mais.*/
	public List getList() {
		return list;
	}
	
	@SuppressWarnings("unchecked")
	/**
	 * Seta uma nova lista ao DataList. Internamente, ele recria uma estrutura de índices para prover a localização das entidades dessa nova lista.
	 * @see find
	 */
	public void setList(List list) {
		dispatchOnBeforeSetList(list);
		hashMapIndexes = new HashMap<String, HashMapIndexList>();
		this.list = list;		
		addEntityPropertyListener();
		dispatchOnAfterSetList(list);
	}
	
	/**
	 * Adiciona o EntityPropertyListener para cada entidade da lista.
	 */
	private void addEntityPropertyListener() {		
		for (Iterator iterator = this.list.iterator(); iterator.hasNext();) {
			AppEntity ent = (AppEntity) iterator.next();
			addEntityPropertyListener(ent);
		}
	}
	
	/**
	 * Adiciona o EntityPropertyListener a entidade informada.
	 */
	private void addEntityPropertyListener(AppEntity ent) {		
		if ( ent.getEntityPropertyListener() != null ){
			ent.getEntityPropertyListener().add(this);
		}
	}

	@SuppressWarnings("unchecked")
	/**
	 * Retorna a estrutura de indices. Também não deve ser manipulada diretamente.
	 */
	public HashMap getHashMapIndexes() {
		return hashMapIndexes;
	}	
	
	/***
	 * @param values
	 * @return Pesquisa em todos os beans da lista desse DataList, retornando true se algum dos beans possuir o Id passado como parametro.
	 * @throws Exception
	 * @see find
	 */
	public Boolean findId( Object object ) throws Exception{
		HashMapIndexList index = this.getIndexByFieldNames( "id" );		
		return index.find(object);
	}
	
	public AppEntity findEntityById( Object values ) throws Exception {
		HashMapIndexList index = this.getIndexByFieldNames( "id" );		
		Collection<AppEntity> findObjectByKey = index.findObjectByKey(values);
		for (Iterator iterator = findObjectByKey.iterator(); iterator.hasNext();) {
			AppEntity appEntity = (AppEntity) iterator.next();
			return appEntity;
		}
		return null;
	}

	/***
	 * @param values
	 * @return Pesquisa em todos os beans da lista desse DataList, retornando true se algum dos beans possuir os valores passados como parametro através do "object..." nas colunas passadas em groupIndexFieldNames.
	 * Exemplos: 
	 * .........................................
	 * DataList dl = DataListFactory.newDataList(database)
	 * dl.executeQuery("from Bean")
	 * if ( dl.find( "grupo;classe", "Grupo A", 123456 ) ){
	 *    ArrayList<Bean> beans = (ArrayList<Bean>)dl.getRange( "grupo;classe", "Grupo A", 123456 );
	 *    ...
	 * }  
	 * .........................................
	 * @see getOne
	 * @see getRange
	 * @throws Exception
	 */
	public Boolean find( String groupIndexFieldNames, Object... values ) throws Exception{
		HashMapIndexList index = this.getIndexByFieldNames( groupIndexFieldNames );		
		return index.find(values);
	}
	
	/**
	 * Returns a ArrayList with Objects found in index.
	 * Retorna uma visão (ArrayList) filtrada da lista do DataList contendo os objetos que correspondem com o filtro passado atraves das colunas no parametro groupIndexFieldNames e dos valores através do "object..." 
	 * @throws Exception
	 * @see find 
	 * @see getOne
	 * @see getRange
	 * @see getObjectById
	 */
	@SuppressWarnings("unchecked")
	public HashMap<Long,AppEntity> getRange( String groupIndexFieldNames, Object... values ) throws Exception{
		HashMapIndexList index = this.getIndexByFieldNames( groupIndexFieldNames );		
		return index.get(values);
	}	

	public Collection<AppEntity> getRangeAsCollection( String groupIndexFieldNames, Object... values ) throws Exception{
		HashMap<Long,AppEntity> range = this.getRange(groupIndexFieldNames, values);
		return (range == null ? null : range.values());
	}	
	
	@SuppressWarnings("unchecked")
	/**
	 * Retorna o primeiro objeto, ou único, que atenda aos critérios passados. Funciona semelhante ao find, mas retornando o objeto encontrado.
	 * Se o objeto não for encontrado retorna null.
	 * Observação, deve ser feito casting para o objeto desejado.
	 * @see find 
	 * @see getObjectById
	 * @see getRange
	 */
	public AppEntity getOne( String groupIndexFieldNames, Object... values ) throws Exception{
		HashMap<Long,AppEntity> list = this.getRange(groupIndexFieldNames, values);
		if ( list == null || list.isEmpty()  ){
			return null;
		}
		return list.values().iterator().next();
	}	
	
	@SuppressWarnings("unchecked")
	/**
	 * Retorna o objeto da lista cujo id é igual ao id passado.
	 * Se nao encontrar, retornará null.
	 * Observação, deve ser feito casting para o objeto desejado.
	 */
	public AppEntity getObjectById(Long id) throws Exception{
		return this.getOne("id", id);
	}	

	/**
	 * @param fieldNames é preenchido com uma string de nomes de campos separados por ponto e vírgula.
	 * Método interno que chama a criacao do indice e retorna o indice correspondente ao nomes passados.
	 */
	private HashMapIndexList getIndexByFieldNames(String fieldNames) throws Exception {
		buildHashMapIndexIfNotExists(fieldNames);
		return this.hashMapIndexes.get(fieldNames);
	}	
	
	/**
	 * @param fieldNames é preenchido com uma string de nomes de campos separados por ponto e vírgula.
	 * Cria um indice novo caso ele ja nao exista na estrutura interna de indices.
	 */
	private void buildHashMapIndexIfNotExists( String fieldNames ) throws Exception{
		//TODO IMPORTANTE!!! VERIFICAR se o fieldNames nao deveria ser convertido a caixa baixa ou caixa alta pois podemos estar criando
		//dois indices caso passemos dois fieldnames assim: "NOME;CODIGO" ou "nome;codigo" ou "Nome;Codigo".
		if ( this.hashMapIndexes.get(fieldNames) == null ){
			HashMapIndexList index = new HashMapIndexList( this.list, fieldNames );
			index.build();
			this.hashMapIndexes.put(fieldNames, index );
		}
	}	
	/**
	 * Executa uma query HQL e seta a lista de retorno vinda do banco no dataList.
	 * Exemplo 1:
	 * DataList dl = DataListFactory.newDataList(database);
	 * dl.executeQuery("from " + Bean.class.getName() ); //Trará todos os beans executando uma query no banco.
	 * 
	 * Exemplo 2:
	 * DataList dl = DataListFactory.newDataList(database);
	 * dl.executeQuery("from " + Bean.class.getName() + " where nome like '" + nome + "%'"); //Trará todos os beans filtando pelo começo do nome passado executando uma query no banco.
	 * 
	 * @param hquery uma string contendo uma instrução HQL.
	 * @return A list with all objects from the query.
	 * @see setList
	 */
	@SuppressWarnings({ "unchecked" })
	public List executeQuery(String hquery) throws RuntimeException{
		try{			
			this.setList(transactionalSession.executeQuery(hquery));		
			return this.list;
		} catch(Exception e){
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Executa uma query retornando um número máximo de registros
	 * 
	 * @author eduardo
	 * @param hquery
	 * @param offset registro inicial, caso não precise, informar 0
	 * @param maxResults número máximo de registros
	 * @return
	 * @throws RuntimeException
	 */
	public List executeQuery(String hquery, int offset, int maxResults) throws RuntimeException{
		try{			
			this.setList(transactionalSession.executeQuery(hquery, offset, maxResults));		
			return this.list;
		} catch(Exception e){
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Método interno que commita as informações no banco.
	 * A idéia é que o método faça:
	 * 1) Inicie uma transação no transactional session.
	 * 2) Salva todos os objetos da lista savedEntities na sessao.
	 * 3) Deleta todos objetos da lista deletedEntities da sessao.
	 * 4) Chama o commit da transacao.
	 * 5) Limpa as listas savedEntities e deletedEntities.
	 * @param transactionalSession
	 * @return
	 * @throws Exception
	 * @see saveAndDeleteEntitiesOnSession
	 */
	protected Long commitByAppTransaction(AppTransaction transactionalSession) throws Exception {	
		try {
			transactionalSession.beginTransaction();
				
			saveAndDeleteEntitiesOnSession( transactionalSession );
			
			Long transaction = null;
			
			try {
				transaction = transactionalSession.commit();
			} catch (Exception e) {
				transactionalSession.clear();
				e.printStackTrace();
				throw new CommitException("Não foi possível gravar as alterações no banco de dados.", e);
			}
			
			this.savedEntities.clear();
			this.deletedEntities.clear();
			
			return transaction;
		} catch (Exception e) {
			System.out.println("transacao delete: " + transactionalSession.toString());
			System.out.println("sessao delete: " + transactionalSession.getSession().toString());
			transactionalSession.rollback();
			throw e;
		}	
	}

	/**
	 * Metodo interno chamado pelo commitByAppTransaction.
	 * @param transactionalSession
	 * @see commitByAppTransaction
	 */
	private void saveAndDeleteEntitiesOnSession( AppTransaction transactionalSession ){
		Set<Long> entities = this.savedEntities.keySet();
		for (Iterator iterator = entities.iterator(); iterator.hasNext();) {
			Long id = (Long) iterator.next();
			Object object = this.savedEntities.get(id);
			boolean tempEntity = ReflectUtil.inheritsFrom(object.getClass(), AppTempEntity.class);
			if ( ! tempEntity){				
				try {
					transactionalSession.save((AppEntity)object);
				} catch (Exception e) {
					e.printStackTrace();
					throw new SaveException("Não foi possível salvar o registro " + object.toString(), e);
				}
			}
		}
		
		/*for (Iterator iterator = this.savedEntities.keySet().iterator(); iterator.hasNext();) {
			Object object = (Object) iterator.next();
			boolean tempEntity = ReflectUtil.inheritsFrom(object.getClass(), AppTempEntity.class);
			if ( ! tempEntity){				
				transactionalSession.save((AppEntity)object);
			}
		}*/	

		entities = this.deletedEntities.keySet();
		for (Iterator iterator = entities.iterator(); iterator.hasNext();) {
			Long id = (Long) iterator.next();
			Object object = this.deletedEntities.get(id);
			boolean tempEntity = ReflectUtil.inheritsFrom(object.getClass(), AppTempEntity.class);
			if ( ! tempEntity){				
				try {
					transactionalSession.delete((AppEntity)object);
				} catch (Exception e) {
					e.printStackTrace();
					throw new DeleteException("Não foi possível deletar o registro " + object.toString(), e);
				}
			}
		}	
		
		/*for (Iterator iterator = this.deletedEntities.iterator(); iterator.hasNext();) {
			Object object = (Object) iterator.next();
			boolean tempEntity = ReflectUtil.inheritsFrom(object.getClass(), AppTempEntity.class);
			if ( ! tempEntity){				
				transactionalSession.delete((AppEntity)object);
			}
		}*/	
		
	}
	
	/**
	 * Salva as entidades modificadas neste DataList no banco.
	 * @return retorna o long que representa o ID da transação executada no banco.
	 * @throws Exception
	 */
	public Long commit() throws Exception {	
		return this.commitByAppTransaction( this.transactionalSession );
	}
	
	/**
	 * Salva as entidades modificadas na lista de DataLists no banco.
	 * Uma observação importante é que todos os dataLists devem compartilhar a mesma transactionalSession. Caso isso não ocorra o sistema subirá um erro.
	 * Restrições para execução de commit de vários dataLists:
	 * 1) Todos os dataLists devem possuir a mesma transação.
	 * ....
	 * Exemplo:
	 * 	DataList registrosJaGravados = DataListFactory.newDataList(database);
	 *	DataList novosRegistros      = DataListFactory.newDataList(registrosJaGravados.getTransactionalSession());
	 *	DataList registrosAlterados  = DataListFactory.newDataList(registrosJaGravados.getTransactionalSession());
	 *	DataList registrosDeletados  = DataListFactory.newDataList(registrosJaGravados.getTransactionalSession());
	 *... 
	 *		DataList.commit(
	 *			registrosJaGravados, 
	 *			novosRegistros,
	 *			registrosAlterados, 
	 *			registrosDeletados);
	 * ...
	 * @return retorna o long que representa o ID da transação executada no banco.
	 * @throws Exception
	 */
	public static Long commit( DataList... dataLists ) throws Exception {	
		if ( dataLists.length != 0 ){
			AppTransaction transactionalSession = dataLists[0].transactionalSession;

			checkDataListsToCommit( transactionalSession, dataLists );
			
			try {
				transactionalSession.beginTransaction();

				for (int i = 0; i < dataLists.length; i++) {
					dataLists[i].saveAndDeleteEntitiesOnSession( transactionalSession );
				}

				Long transaction = transactionalSession.commit();

				for (int i = 0; i < dataLists.length; i++) {
					dataLists[i].savedEntities.clear();
					dataLists[i].deletedEntities.clear();
				}

				return transaction;
			} catch (Exception e) {
				transactionalSession.rollback();
				
				throw e;
			}
		}
		return null;
	}

	/**
	 * Metodo interno que verifica se commit de vários dataLists juntos pode ser executao. 
	 * Restrições para execução de commit de vários dataLists:
	 * 1) Todos os dataLists devem possuir a mesma transação.
	 * @param transactionalSession
	 * @param dataLists
	 * @throws Exception
	 */
	private static void checkDataListsToCommit(AppTransaction transactionalSession, DataList... dataLists) throws Exception {
		for (int i = 0; i < dataLists.length; i++) {
			if ( dataLists[i].transactionalSession == null ){
				throw new Exception( "DataList " + i + " has transactionalSession null. Cannot execute commit." );
			}
			
			if ( ! dataLists[i].transactionalSession.equals(transactionalSession) ){
				throw new Exception( "DataList " + i + " has transactionalSession different from others. Cannot exeute commit." );
			}	
		}
	}
	
	/**
	 * Verifica se a ação de persistir a entidade não fere uma contraint de valor único.
	 * @param entity A entidade que pode ou não violar a constraint caso seja persistida.
	 * @return verdadeiro se a ação de persistir a entidade for válida; falso, caso contrário.
	 * 
	 * Por Renan, em 6/8/2010 
	 */
	public boolean validUnique(AppEntity entity){
		/* Teste de constraint unique em lógica de negócio - Renan
		 * */
		if (entity.getClass().getAnnotation(UniqueValidation.class) != null){
			
			//Recuperamos a annotation que contém os campos da constraint Unique.
			UniqueValidation unique = entity.getClass().getAnnotation(UniqueValidation.class);
			
			/* Montamos uma query com as seguintes regras: deve retornar elementos com a chave única
			 * e um ID diferente do elemento. Se a query retornar uma linha, é porque há violação
			 * da constraint Unique.
			 */
			String query = "SELECT 1 FROM " + entity.getClass().getName() + " WHERE";
			List<Object> parameters = new ArrayList<Object>();
			
			for (int i = 0; i < unique.fields().length; i++){
				String field = unique.fields()[i];			
				
				query += " " + field + " = ? AND";				
				parameters.add(ReflectUtil.getFieldValue(entity, field));
			}			
			query = query.substring(0, query.length() - 3);
			
			query += "AND id <> ?";
			parameters.add(entity.getId());
			
			//Executamos a query através da sessão transacional.
			List result = getTransactionalSession().executeQuery(query, parameters);
			
			//Caso a query retorne ao menos um elementos, temos uma violação.
			if (result.size() > 0){
				return false; 
			}
		}
		
		//Se chegamos aqui, a persistência é válida.
		return true;
		/* Fim do teste de constraint unique em lógica de negócio - Renan
		 * */
	}

	/**
	 * Save information in a TransactionalSession and rebuild indexes.
	 * Um save deve ser dado toda vez que uma entidade for alterada.
	 * Ex:
	 * Entidade entidade = (Entidade)dataListEntidades.getOne( "id", 123L );
	 * ...
	 * entidade.setNome( "nome" );
	 * entidade.setInicio( new Inicio() );
	 * dataListEntidades.save( entidade );
	 * ...
	 * dataListEntidades.commit();
	 * 
	 * @param entity
	 * @throws Exception 
	 * @see add
	 * @see commit
	 */
	public void save(AppEntity entity) throws Exception {
		boolean tempEntity = ReflectUtil.inheritsFrom(entity.getClass(), AppTempEntity.class);
			
		//TODO IMPORTANTE colocar validação para impedir que alguém tente salvar uma entidade que não está na lista.
		
		//Testamos se a ação de persistir a entidade não fere uma constraint única -Renan
		if (this.validUnique(entity) == false){
			throw new Exception("Unique Constraint Violation!");
		}
		
		this.rebuildGroupIndexFromObjectChange(entity);
		
		if ( this.logChanges && this.savedEntities.get(entity.getId()) == null ){
			this.savedEntities.put( entity.getId(), entity );
		}
		
		if (!tempEntity){			
			if ( this.commitOnSave ){
				this.commit();
			}
		}
	}	
		
	/**
	 * Cria uma nova entidade para a classe especificada,  além de adicionar an lista interna do DataList. 
	 * Também é criado um Id com o valor dentro da faixa da licenca especiicada para este objeto.
	 * O id será gerado a partir do banco de dados especificado pelo parametro.
	 * @param beanName
	 * @return
	 * @throws Exception
	 */
	public AppEntity newEntity(String database, String beanName, Long licenseToCreateKey) throws Exception{		
		Class c = Class.forName(beanName);
		AppEntity baseEntity = (AppEntity)c.newInstance();
		
		dispatchOnBeforeNewEntity(baseEntity);
		
		baseEntity.createId(database, licenseToCreateKey, null );		
		this.add(baseEntity);
		
		dispatchOnAfterNewEntity(baseEntity);
		
		return baseEntity;
	}	
	
	/**
	 * Esse método difere do newEntity(String database, String beanName, Long licenseToCreateKey) apenas
	 * pelo fato de já receber a entidade que será adicionada. Isso facilita nos casos em que a criação do
	 * bean é feito por outro process, mas o eventos da grid devem ser disparados, e não apenas adicionar o bean
	 * na lista.
	 * @param entity
	 * @return
	 * @throws Exception
	 */
	public AppEntity newEntity(AppEntity entity ) throws Exception{		
		dispatchOnBeforeNewEntity(entity);
		
		this.add(entity);		
		dispatchOnAfterNewEntity(entity);
		
		return entity;
	}
	
	/**
	 * Cria uma nova entidade para a classe especificada,  além de adicionar an lista interna do DataList. 
	 * Também é criado um Id com o valor dentro da faixa da licenca especiicada para este objeto.
	 * @param beanName
	 * @param licenseToCreateKey id da licença a ser criada a chave.
	 * @return
	 * @throws Exception
	 */
	public AppEntity newEntity(String beanName, Long licenseToCreateKey) throws Exception{
		return this.newEntity(this.transactionalSession.getDatabase(), beanName, licenseToCreateKey);
	}

	/**
	 * Cria uma nova entidade para a classe especificada, além de adicionar an lista interna do DataList. Também é criado um Id positivo para este objeto.
	 * @param beanName
	 * @return O objeto recém criado. NO retrono deve ser dado um casting para a classe desejada.
	 * @throws Exception
	 */
	public AppEntity newEntity(String beanName) throws Exception{
		return this.newEntity(this.transactionalSession.getDatabase(), beanName, null);
	}

	@SuppressWarnings("unchecked")
	/**
	 * Adiciona um registro na lista deste DataList e garante a atualização dos indices.
	 * @throws Sobe um erro caso o objeto adicionado ja esteja na lista.
	 **/	
	public void add( AppEntity appEntity ) throws Exception{	
		dispatchOnBeforeAddEntityOnList(appEntity);
		
		if ( this.findId( appEntity.getId() ) ){
			throw new Exception("Entity already exists in list.");
		}
		
		this.addEntityAndRebuildIndex( appEntity );
		this.addEntityPropertyListener(appEntity);
		
		dispatchOnAfterAddEntityOnList(appEntity);
	}	
	
	/**
	 * Adiciona a entidade na lista, e refaz os índices do datalist.
	 * @param appEntity
	 * @throws Exception
	 */
	public void addEntityAndRebuildIndex( AppEntity appEntity ) throws Exception{
		this.list.add( appEntity );
		this.rebuildGroupIndexFromObjectInsert(appEntity);
	}
	
	/**
	 * Executa o método DataList.add() e executa um DataList.save() logo após.
	 * Cuidado ao executar este método caso o commitOnSAve esteja true.
	 * @see add()
	 * @see save()
	 **/		
	public void addAndSave( AppEntity appEntity ) throws Exception{
		this.add( appEntity );
		this.save( appEntity );
	}
	
	/**
	 * Adiciona um todos os objetos do dataList passado como parametro, objeto a objeto, neste dataList.
	 * @param dataList
	 * @throws Exception
	 */
	public void add( DataList dataList ) throws Exception{
		List list = dataList.getList();
		for (Iterator iterator = list.iterator(); iterator.hasNext();) {
			AppEntity appEntity = (AppEntity) iterator.next();
			this.add( appEntity );
		}
	}	
	
	/**
	 * Adiciona um dataList todo, objeto a objeto, neste dataList já chamado o save.
	 * Cuidado ao executar este método caso o commitOnSAve esteja true.
	 * @param dataList
	 * @throws Exception
	 * @see add
	 * @see save
	 */
	public void addAndSave( DataList dataList ) throws Exception{
		List list = dataList.getList();
		for (Iterator iterator = list.iterator(); iterator.hasNext();) {
			AppEntity appEntity = (AppEntity) iterator.next();
			this.addAndSave( appEntity );
		}
	}	
	
	
	/**
	 * Deleta o objeto passado
	 * @param object
	 * @return
	 * @throws Exception
	 */
	public Boolean delete(AppEntity object) throws Exception{ 
		dispatchOnBeforeDeleteEntity(object);
		
		boolean tempEntity = ReflectUtil.inheritsFrom(object.getClass(), AppTempEntity.class);
		
		int index = this.list.indexOf(object);
		
		if ( index != -1 ){
			this.list.remove(index);
			
			if (this.logChanges && !tempEntity && transactionalSession != null) {				
				transactionalSession.delete((AppEntity)object);
			}
		}	

		//TODO IMPORTANTE!!! verificar se o restante desse metodo aqui em baixo, nao deveria ficar dentro do if ai de cima, que faz com que ele só remova da lista caso exista na lista.
		this.rebuildGroupIndexFromObjectDelete(object);
		
		if ( this.logChanges && this.deletedEntities.get(object.getId()) == null ){
			this.deletedEntities.put( object.getId(), object );
		}

		if ( ! tempEntity ){				
			if ( this.commitOnSave ){
				this.commit();
			}
		}
		
		dispatchOnAfterDeleteEntity(object);
		
		return true;
	}		
	
	/**
	 * This method deletes all objects from this dataList.
	 * @return Returns the number of deleted objects.
	 * @throws Exception 
	 * @see delete
	 */
	public Long empty() throws Exception{
		dispatchOnBeforeEmpty();
		
		//Coloquei isso para que o método empty não eclua nada do banco de dados
		Boolean originalLogChanges = getLogChanges();
		setLogChanges(false);	
		Long count = 0L;
		
		try{			
			while ( ! this.isEmpty() ){
				this.delete( (AppEntity)this.list.get(0) );
				count++;
			}
		} finally {
			setLogChanges(originalLogChanges);
		}
		
		dispatchOnAfterEmpty();
		
		return count;
	}
	
	@SuppressWarnings("unchecked")
	/**
	 * Metodo interno que refaz os indices para um determinado objeto passado. 
	 * Este método é chamado dentro do save.
	 * @see save
	 * @see rebuildGroupIndexFromObjectInsert
	 * @see rebuildGroupIndexFromObjectDelete
	 */
	private void rebuildGroupIndexFromObjectChange(AppEntity object) throws Exception {
		if ( ! hashMapIndexes.isEmpty() ){
			Set<String> keys = hashMapIndexes.keySet();
			for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
				String key = (String) iterator.next();
				HashMapIndexList groupIndex = hashMapIndexes.get(key);
				groupIndex.reIndexObject( object );
			}			
		}
	}	

	@SuppressWarnings("unchecked")
	/**
	 * Metodo interno que refaz os indices para um determinado objeto passado. 
	 * Este método é chamado dentro do add.
	 * @see add
	 * @see rebuildGroupIndexFromObjectChange
	 * @see rebuildGroupIndexFromObjectDelete
	 */
	private void rebuildGroupIndexFromObjectInsert(AppEntity object) throws Exception {
		if ( ! hashMapIndexes.isEmpty() ){
			Set<String> keys = hashMapIndexes.keySet();
			for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
				String key = (String) iterator.next();
				HashMapIndexList groupIndex = hashMapIndexes.get(key);
				groupIndex.insertObject( object );
			}			
		}
	}	

	@SuppressWarnings("unchecked")
	/**
	 * Metodo interno que refaz os indices para um determinado objeto passado. 
	 * Este método é chamado dentro do delete.
	 * @see delete
	 * @see empty 
	 * @see rebuildGroupIndexFromObjectChange
	 * @see rebuildGroupIndexFromObjectInsert
	 */
	private void rebuildGroupIndexFromObjectDelete(AppEntity object) throws Exception {
		if ( ! hashMapIndexes.isEmpty() ){
			Set<String> keys = hashMapIndexes.keySet();
			for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
				String key = (String) iterator.next();
				HashMapIndexList groupIndex = hashMapIndexes.get(key);
				groupIndex.deleteObject( object );
			}			
		}
	}		
	
	/**
	 * Retorna Set com todas as distintas "chaves" para acessar os grupos de objetos definidos pelos groupIndexFieldNames;
	 * @param groupIndexFieldNames Lista de nomes de campos separados por ponto e vírgula.
	 * @return 
	 * @throws Exception
	 */
	public Set<String> getKeySetFromGroupIndex(String groupIndexFieldNames) throws Exception{
		buildHashMapIndexIfNotExists(groupIndexFieldNames);
		HashMapIndexList groupIndex = hashMapIndexes.get(groupIndexFieldNames);
		return groupIndex.getMap().keySet();
	}

	/**
	 * Limpa todos os indices criados. É interessante executar este método quando tivermos certeza que nao queremos mais usar os indices de um datalist.
	 */
	public void clearGroupIndexes(){
		hashMapIndexes.clear();
	}	
	
	public void sort(final String... fields) {
		DataList.sort(list, fields);
	}	
	
	/**
	 * Ordena uma lista, baseado no comparator passado como parâmetro. Ex:
	 * DataList.sort(list, "codigo", "nome", "data DESC")
	 * @param list
	 * @param fields
	 * @param comparator
	 * @throws RuntimeException
	 */
	@SuppressWarnings("unchecked")
	public static void sort(List list, Comparator comparator) throws RuntimeException{
		Collections.sort(list, comparator);
	}
	
	/**
	 * Ordena uma lista. Ex:
	 * DataList.sort(list, "codigo", "nome", "data DESC")
	 * @param list
	 * @param fields
	 * @throws RuntimeException
	 */
	@SuppressWarnings("unchecked")
	public static void sort(List list, final String... fields) throws RuntimeException{
		Collections.sort(list, createComparator(fields));
	}
	
	@SuppressWarnings("unchecked")
	public static Comparator createComparator( final String...fields ){
		Comparator comp = new Comparator(){
			@Override
			public int compare(Object o1, Object o2) {
				Object value1 = null;
				Object value2 = null;
				String compareString1 = "";
				String compareString2 = "";
				
				for (int i = 0; i < fields.length; i++) {
					try{			
						String fieldDef[] = StringUtils.split(fields[i]);							
						value1 = ReflectUtil.getFieldValue(o1, fieldDef[0]);
						value2 = ReflectUtil.getFieldValue(o2, fieldDef[0]);
						
						//-- Tratamento lookup
						if ( value1 instanceof AppEntity ){
							value1 = ((AppEntity)value1).toString();
						}
						if ( value2 instanceof AppEntity ){
							value2 = ((AppEntity)value2).toString();
						}
						
						String result1 = FIRST, result2 = FIRST;		
						
						int compResult = 0;
						if (value1 == null && value2 != null){							
							compResult = -1;							
						} else if (value1 != null && value2 == null){
							compResult = 1;
						} else if (value1 != null && value2 != null){
							if (value1 instanceof String && value2 instanceof String){
								compResult = ((String)value1).compareToIgnoreCase((String)value2);
							} else if (value1 instanceof Comparable){																
								compResult = ((Comparable)value1).compareTo(value2);
							} else {
								compResult = ComparableComparator.getInstance().compare(value1, value2);
							}
						}
						
						if (compResult < 0){
							if (fieldDef.length > 1 && fieldDef[1].equalsIgnoreCase("DESC")){
								result1 = LAST;
							}else{								
								result2 = LAST;
							}
						} else if (compResult > 0){
							if (fieldDef.length > 1 && fieldDef[1].equalsIgnoreCase("DESC")){
								result2 = LAST;
							}else{								
								result1 = LAST;
							}
						}
												
						compareString1 += result1;
						compareString2 += result2;
					}catch(Exception e){
						throw new RuntimeException(e);
					}
				}				
								
				return compareString1.compareToIgnoreCase(compareString2);
			}			
		};
		return comp;
	}
	
	public Boolean getCommitOnSave() {
		return commitOnSave;
	}
	
	/**
	 * Indica se o dataList deve sempre commitar as informações no banco após um save. 
	 * Muito cuidado ligando este método, pois qualquer for dando um save, serão feitas n conexões com o banco.
	 */
	public void setCommitOnSave(Boolean commitOnSave) {
		this.commitOnSave = commitOnSave;
	}
	
	public void setLogChanges(Boolean logChanges) {
		this.logChanges = logChanges;
	}
	
	public Boolean getLogChanges() {
		return this.logChanges;
	}

	public HashMap<Long, AppEntity> getDeletedEntities(){
		return deletedEntities; 
	}
	
	public HashMap<Long, AppEntity> getSavedEntities(){
		return savedEntities; 
	}

	/**
	 * @return Retorna a informação dizendo se este datalist possui uma lista vazia ou nao. Retornará true caso a lista seja null.
	 */
	public Boolean isEmpty(){
		if ( this.list == null ){
			return true;
		}
		return this.list.isEmpty();
	}
	
	public AppTransaction getTransactionalSession(){
		return this.transactionalSession;
	}
	
	public void setTransactionalSession(AppTransaction transactionalSession) {
		this.transactionalSession = transactionalSession;
	}
	
	/**
	 * Returns all distinct values from column especified by parameter fieldName of all objects of this datalist.
	 * Retorna todos os valores distintos da coluna especificada pelo parametro fieldName de todos os objetos deste datalist.
	 **/
	public Set<String> getDistinctValues(String fieldName) throws Exception {
		HashMapIndexList index = this.getIndexByFieldNames( fieldName );				
		return index.getMap().keySet();
	}
	
	/**
	 * @author danilo sampaio
	 * @throws Exception 
	 * @since 15/12/2009
	 * <hr>
	 * <pre>
	 * Funcao.....: esta função tem a finalidade de filtrar um datalist usando SQL.
	 * Sistema....: infra
	 * Requisito..: 2987961 - 02 - Liberar no DataList.filter() usando a API de SQL para Coleções.
	 * Observação.: Exemplo de Uso:
	 * 				dataList.filter(User.class, "where name = 'teste' and id > 0");
	 * </pre>
	 */
	public List filter( Class clazz, String whereExpress ) throws Exception{
		if ( clazz == null ){
			throw new Exception("clazz parameter must be informed to filter datalist.");
		}
		
		org.josql.Query query = new org.josql.Query();
		String selectExpess = "Select * from " + clazz.getName() + " ";
		
		whereExpress = whereExpress.indexOf("where") != -1 ? whereExpress : (" where 1=1 " + whereExpress);
		System.out.println(selectExpess + whereExpress);
		query.parse(selectExpess + whereExpress);
		QueryResults result = query.execute(getList());
		return result.getResults();
	}
	
	/* *************************************************************************
	 ************************** DATALIST BINDING *******************************
	 ****************************************************************************
	 */
	/**
	 * Retorna os listeners deste datalist
	 * @return
	 */
	public ArrayList<DataListListener> getDataListListeners() {
		return dataListListeners;
	}
	
	/**
	 * Seta os listeners do datalist
	 * @param dataListListeners
	 */
	public void setDataListListeners(ArrayList<DataListListener> dataListListeners) {
		this.dataListListeners = dataListListeners;
	}
	
	/**
	 * Adiciona um listener na lista
	 * @param listener
	 */
	public synchronized void addDataListListener( DataListListener listener){
		if ( ! dataListListeners.contains(listener)){
			dataListListeners.add(listener);
		}
	}
	
	/**
	 * Remove um listener da lista
	 * @param listener
	 */
	public synchronized void removeDataListListener( DataListListener listener){
		dataListListeners.remove(listener);
	}
	
	/**
	 * Dispara o onBeforeSetList() dos listeners do datalist.
	 * @param list
	 * @throws Exception 
	 */
	private void dispatchOnBeforeSetList( List list ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onBeforeSetList(event, list);
		}
	}
	/**
	 * Dispara o onAfterSetList() dos listeners do datalist.
	 * @param list
	 * @throws Exception 
	 */
	private void dispatchOnAfterSetList( List list ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onAfterSetList(event, list);
		}
	}
		
	/**
	 * Dispara o onBeforeNewEntity() dos listeners do datalist.
	 * @param list
	 */
	private void dispatchOnBeforeNewEntity( AppEntity baseEntity ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onBeforeNewEntity(event, baseEntity);
		}
	}
	/**
	 * Dispara o onAfterNewEntity() dos listeners do datalist.
	 * @param list
	 */
	private void dispatchOnAfterNewEntity( AppEntity baseEntity ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onAfterNewEntity(event, baseEntity);
		}
	}
	
	/**
	 * Dispara o onBeforeDelete() dos listeners do datalist.
	 * @param list
	 */	
	private void dispatchOnBeforeDeleteEntity( AppEntity entity ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onBeforeDeleteEntity(event, entity);
		}
	}
	/**
	 * Dispara o onAfterDelete() dos listeners do datalist.
	 * @param list
	 */
	private void dispatchOnAfterDeleteEntity( AppEntity entity ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onAfterDeleteEntity(event, entity);
		}
	}
	
	/**
	 * Dispara o onBeforeEmpty() dos listeners do datalist.
	 * @param list
	 */	
	private void dispatchOnBeforeEmpty(){
		if ( this.getList().size() > 0 ){
			ArrayList<DataListListener> listeners;
			
			synchronized (this) {
				listeners = (ArrayList<DataListListener>) dataListListeners.clone();
			}
			
			DataListEvent event = new DataListEvent(this);
			for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
				DataListListener dataListListener = (DataListListener) iterator.next();
				dataListListener.onBeforeEmpty(event);
			}
		}	
	}
	/**
	 * Dispara o onAfterEmpty() dos listeners do datalist.
	 * @param list
	 */
	private void dispatchOnAfterEmpty(){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onAfterEmpty(event);
		}
	}
	
	/**
	 * Dispara o onBeforeAddEntityOnList() dos listeners do datalist.
	 * @param entity
	 */
	private void dispatchOnBeforeAddEntityOnList( AppEntity entity ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onBeforeAddEntityOnList(event, entity);
		}
	}
	
	/**
	 * Dispara o onAfterAddEntityOnList() dos listeners do datalist.
	 * @param entity
	 */
	private void dispatchOnAfterAddEntityOnList( AppEntity entity ){
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}
		
		DataListEvent event = new DataListEvent(this);
		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onAfterAddEntityOnList(event, entity);
		}
	}
	
	/**
	 * Dispara os listeners para a alteração de uma propriedade do bean.
	 */
	@Override
	public void onAfterChangeProperty(EntityPropertyEvent event, String propertyName, Object value) {
		AppEntity ent = (AppEntity) event.getSource();
		System.out.println(">>> Propriedade	" + propertyName + " da entidade " + ent.toString() + 
				" foi alterada para: " + ( value == null ? "nulo" : value.toString()) );
		
		ArrayList<DataListListener> listeners;
		
		synchronized (this) {
			listeners = (ArrayList<DataListListener>) dataListListeners.clone();
		}

		for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
			DataListListener dataListListener = (DataListListener) iterator.next();
			dataListListener.onAfterChangeEntityProperty(event, propertyName, value);
		}
	}	

	
	public DataListView getView(String groupIndexFieldNames, Object... values) throws Exception{
		if ( groupIndexFieldNames == null || groupIndexFieldNames.isEmpty() ){
			throw new Exception("GroupIndexFieldNames cannot be null.");
		}
		
		DataListView view = null;		
		String lowerGroupIndexFieldNames = groupIndexFieldNames.toLowerCase();
		if ( this.views.containsKey(lowerGroupIndexFieldNames) ){
			view = this.views.get(lowerGroupIndexFieldNames);
		} else {
			view = new DataListView( this, groupIndexFieldNames, values );
		}

		this.rebuildView( view, this, lowerGroupIndexFieldNames, values );
		
		return view;
	}

	protected void rebuildView(DataList view, DataList parent, String lowerGroupIndexFieldNames, Object... values ) throws Exception{
		//saves the original logChanges
		Boolean originalLogChangesFromView = view.getLogChanges();
		
		//Set to false to call empty
		view.setLogChanges(false);
		view.empty();
		
		HashMap<Long,AppEntity> range = this.getRange(lowerGroupIndexFieldNames, values);
		if ( range != null ){
			Collection<AppEntity> objects = range.values();
			for (Iterator iterator = objects.iterator(); iterator.hasNext();) {
				AppEntity appEntity = (AppEntity) iterator.next();
				view.add(appEntity);
			}
		}
		
		//returns original logChanges to view
		view.setLogChanges(originalLogChangesFromView);
	}
	
	/**
	 * Pesquisa por uma propriedade dentro da outra caso a string esteja separada por pontos
	 * 
	 * @param object
	 * @param fieldName
	 * @return
	 */
	protected Object getFieldValue(Object object, String fieldName) {
		final String FIELD_SEPARATOR = ".";
		String props[] = StringUtils.split(fieldName, FIELD_SEPARATOR);
		
		if (props != null && props.length > 1) {			
			for (String temp : props) {
				if (object == null) {
					return null;
				}
				object = ReflectUtil.getFieldValueIgnoreCase(object, temp);
			}
			
			return object;
		}
		
		return ReflectUtil.getFieldValueIgnoreCase(object, fieldName);
	}
}